<?php
/**
 * HTML转图片服务
 * 
 * 一个专业的PHP工具类，用于将HTML文件转换为各种格式的图片
 * 支持透明背景、模板数据注入和多种输出格式控制
 * 
 * @package    HtmlToImage
 * @version    1.1.0
 */
namespace common\services\poster\htmlToImage;

class HtmlToImageService
{
    /**
     * Node.js可执行文件路径
     * @var string
     */
    private $nodePath;

    /**
     * 转换脚本路径
     * @var string
     */
    private $scriptPath;

    /**
     * 输出目录路径
     * @var string
     */
    private $outputDir;

    /**
     * 默认输出选项
     * @var array
     */
    private $defaultOutputOptions = [
        'fullPage' => false,      // 完整页面截图
        'element' => true,        // 元素截图（透明背景）
        'elementWithBg' => true,  // 元素截图（白色背景）
        'pdf' => false            // PDF文档
    ];

    /**
     * 默认域名
     * @var string
     */
    private $defaultDomain = '';

    /**
     * 是否启用调试模式
     * @var bool
     */
    private $debug = false;

    /**
     * 临时文件路径列表
     * @var array
     */
    private $tempFiles = [];

    /**
     * 构造函数
     * 
     * @param string $nodePath Node.js可执行文件路径，默认为'node'
     * @param string $scriptPath html2image.js脚本路径
     * @param string $outputDir 输出目录路径，默认为当前目录下的output
     * @param bool $debug 是否启用调试模式
     * @param string $defaultDomain 默认域名
     */
    public function __construct($nodePath = 'node', $scriptPath = null, $outputDir = null, $debug = false, $defaultDomain = '')
    {
        $this->nodePath = $nodePath;
        $this->scriptPath = $scriptPath ?: __DIR__ . DIRECTORY_SEPARATOR . 'html2image.js';
        $this->outputDir = $outputDir ?: __DIR__ . DIRECTORY_SEPARATOR . 'output';
        $this->debug = $debug;
        $this->defaultDomain = $defaultDomain;

        // 确保输出目录存在
        if (!file_exists($this->outputDir)) {
            mkdir($this->outputDir, 0777, true);
        }

        // 检查必要文件是否存在
        $this->checkEnvironment();
    }

    /**
     * 析构函数 - 清理任何剩余的临时文件
     */
    public function __destruct()
    {
        $this->cleanupTempFiles();
    }

    /**
     * 检查环境设置是否正确
     * 
     * @return bool
     * @throws \Exception 如果环境检查失败
     */
    private function checkEnvironment()
    {
        // 检查转换脚本是否存在
        if (!file_exists($this->scriptPath)) {
            throw new \Exception("转换脚本不存在: {$this->scriptPath}");
        }

        return true;
    }

    /**
     * 生成海报图片
     * 
     * @param string $htmlFile HTML文件路径
     * @param array $templateData 模板数据 [可选]
     * @param array $options 附加选项 [可选]
     * @return array 生成结果
     */
    public function generatePoster($htmlFile, array $templateData = null, array $options = [])
    {
        // 合并默认选项
        $options = array_merge([
            'outputPrefix' => null,        // 输出文件名前缀
            'transparent' => true,         // 是否使用透明背景
            'outputOptions' => null,       // 自定义输出选项
            'preset' => null,              // 预设配置 (elements, element-only, full, pdf-only)
            'returnAbsolutePaths' => true, // 是否返回绝对路径
            'cleanup' => true,             // 是否在完成后清理临时文件
            'domain' => $this->defaultDomain, // 域名
            'useCache' => true,            // 是否使用缓存
            'cacheExpire' => 3600 * 24     // 缓存过期时间（默认24小时）
        ], $options);

        // 验证HTML文件是否存在
        $htmlPath = $this->resolvePath($htmlFile);
        if (!file_exists($htmlPath)) {
            throw new \Exception("HTML文件不存在: {$htmlPath}");
        }

        // 生成输出文件前缀
        if (!$options['outputPrefix']) {
            $options['outputPrefix'] = pathinfo($htmlFile, PATHINFO_FILENAME) . '_' . time();
        }

        // 尝试从缓存获取结果
        if ($options['useCache'] && $templateData) {
            $cacheResult = $this->getFromCache($htmlFile, $templateData, $options);
            if ($cacheResult) {
                return $cacheResult;
            }
        }

        // 如果使用预设配置，调用预设方法
        if ($options['preset']) {
            $result = $this->generatePosterWithPreset(
                $options['preset'],
                $htmlFile,
                $options['outputPrefix'],
                $templateData,
                $options
            );

            // 保存到缓存
            if ($options['useCache'] && $templateData && $result['success']) {
                $this->saveToCache($htmlFile, $templateData, $result, $options);
            }

            return $result;
        }

        // 处理模板数据
        $templateFile = null;
        if ($templateData) {
            // 如果提供了domain，添加到模板数据中
            if ($options['domain']) {
                $templateData['domain'] = $options['domain'];
            }

            $templateFile = $this->createTemplateDataFile($templateData);
        }

        // 构建命令
        $command = $this->buildCommand(
            $htmlFile,
            $options['outputPrefix'],
            $options['transparent'],
            $templateFile,
            $options['outputOptions']
        );

        // 执行命令
        $result = $this->executeCommand($command);

        // 根据需要清理临时文件
        if ($options['cleanup']) {
            $this->cleanupTempFiles();
        }

        // 获取HTML文件名作为可能的输出前缀
        $basename = pathinfo($htmlFile, PATHINFO_FILENAME);

        // 检查输出目录中的最新文件
        $potentialFiles = [
            'element' => glob($this->outputDir . DIRECTORY_SEPARATOR . '*_element.png'),
            'elementWithBg' => glob($this->outputDir . DIRECTORY_SEPARATOR . '*_element_with_bg.png'),
            'fullPage' => glob($this->outputDir . DIRECTORY_SEPARATOR . '*_full.png'),
            'pdf' => glob($this->outputDir . DIRECTORY_SEPARATOR . '*.pdf'),
        ];

        // 通过修改时间排序，获取最新的文件
        $outputFiles = [];
        $fileFound = false;

        foreach ($potentialFiles as $type => $files) {
            if (empty($files))
                continue;

            // 按修改时间排序
            usort($files, function ($a, $b) {
                return filemtime($b) - filemtime($a);
            });

            // 先尝试找指定前缀的文件
            $prefixFile = null;
            foreach ($files as $file) {
                if (strpos(basename($file), $basename . '_') === 0) {
                    $prefixFile = $file;
                    break;
                }
            }

            // 如果找到了指定前缀的文件，使用它
            if ($prefixFile) {
                $outputFiles[$type] = $prefixFile;
                $fileFound = true;
            }
            // 否则使用最新的文件
            else if (!empty($files)) {
                $outputFiles[$type] = $files[0];
                $fileFound = true;
            }
        }

        // 如果找到了文件，认为成功，否则认为失败
        $isSuccess = $fileFound;

        if ($this->debug) {
            if ($fileFound) {
                echo "找到生成的文件:\n";
                foreach ($outputFiles as $type => $file) {
                    echo "- {$type}: " . basename($file) . "\n";
                }
            } else {
                echo "未找到任何生成的文件\n";
            }
        }

        // 格式化返回结果
        $finalResult = [
            'success' => $isSuccess,
            'files' => $options['returnAbsolutePaths'] ? $outputFiles : array_map('basename', $outputFiles),
            'output' => $this->debug ? $result['output'] : null,
            'error' => $isSuccess ? null : $result['error'],
            'cached' => false
        ];

        // 保存到缓存
        if ($options['useCache'] && $templateData && $isSuccess) {
            $this->saveToCache($htmlFile, $templateData, $finalResult, $options);
        }

        return $finalResult;
    }

    /**
     * 获取缓存文件路径
     * 
     * @param string $htmlFile HTML文件路径
     * @param array $templateData 模板数据
     * @param array $options 附加选项
     * @return string 缓存路径
     */
    private function getCachePath($htmlFile, array $templateData, array $options)
    {
        // 创建缓存目录（如果不存在）
        $cacheDir = __DIR__ . DIRECTORY_SEPARATOR . 'cache';
        if (!file_exists($cacheDir)) {
            mkdir($cacheDir, 0777, true);
        }

        // 基于HTML文件名和模板数据生成哈希
        $htmlPathHash = md5($htmlFile);
        $templateDataHash = md5(json_encode($templateData));
        $optionsHash = md5(json_encode([
            'transparent' => $options['transparent'],
            'outputOptions' => $options['outputOptions'],
        ]));

        // 组合哈希生成唯一缓存键
        $cacheKey = "{$htmlPathHash}_{$templateDataHash}_{$optionsHash}";

        return $cacheDir . DIRECTORY_SEPARATOR . $cacheKey;
    }

    /**
     * 从缓存获取结果
     * 
     * @param string $htmlFile HTML文件路径
     * @param array $templateData 模板数据
     * @param array $options 附加选项
     * @return array|null 缓存的结果或null
     */
    private function getFromCache($htmlFile, array $templateData, array $options)
    {
        $cachePath = $this->getCachePath($htmlFile, $templateData, $options) . '.json';

        // 检查缓存文件是否存在且未过期
        if (file_exists($cachePath)) {
            $cacheTime = filemtime($cachePath);
            $expireTime = $options['cacheExpire'] ?? 3600 * 24; // 默认24小时

            if (time() - $cacheTime < $expireTime) {
                $cacheData = json_decode(file_get_contents($cachePath), true);

                if ($cacheData && isset($cacheData['files'])) {
                    // 验证缓存的文件是否存在
                    $allFilesExist = true;
                    foreach ($cacheData['files'] as $key => $filePath) {
                        if ($filePath && !file_exists($filePath)) {
                            $allFilesExist = false;
                            break;
                        }
                    }

                    if ($allFilesExist) {
                        $cacheData['cached'] = true;
                        return $cacheData;
                    }
                }
            }

            // 缓存过期或文件不存在，删除缓存
            @unlink($cachePath);
        }

        return null;
    }

    /**
     * 保存结果到缓存
     * 
     * @param string $htmlFile HTML文件路径
     * @param array $templateData 模板数据
     * @param array $result 生成结果
     * @param array $options 附加选项
     */
    private function saveToCache($htmlFile, array $templateData, array $result, array $options)
    {
        $cachePath = $this->getCachePath($htmlFile, $templateData, $options) . '.json';

        // 只缓存成功的结果
        if ($result['success'] && isset($result['files'])) {
            // 创建缓存数据
            $cacheData = [
                'success' => true,
                'files' => $result['files'],
                'cached' => true
            ];

            // 保存到缓存文件
            file_put_contents($cachePath, json_encode($cacheData));
        }
    }

    /**
     * 生成基本海报（不支持模板数据）
     * 
     * @param string $htmlFile HTML模板文件路径
     * @param string $outputPrefix 输出文件名前缀
     * @param bool $transparent 是否使用透明背景
     * @param array $outputOptions 输出选项
     * @return array 生成结果
     * @deprecated 推荐使用 generatePoster() 方法
     */
    public function generateBasicPoster($htmlFile, $outputPrefix = null, $transparent = true, $outputOptions = null)
    {
        return $this->generatePoster($htmlFile, null, [
            'outputPrefix' => $outputPrefix,
            'transparent' => $transparent,
            'outputOptions' => $outputOptions
        ]);
    }

    /**
     * 生成带模板数据的海报
     * 
     * @param string $htmlFile HTML模板文件路径
     * @param array $templateData 模板数据
     * @param string $outputPrefix 输出文件名前缀
     * @param bool $transparent 是否使用透明背景
     * @param array $outputOptions 输出选项
     * @return array 生成结果
     * @deprecated 推荐使用 generatePoster() 方法
     */
    public function generatePosterWithTemplateData($htmlFile, array $templateData, $outputPrefix = null, $transparent = true, $outputOptions = null)
    {
        return $this->generatePoster($htmlFile, $templateData, [
            'outputPrefix' => $outputPrefix,
            'transparent' => $transparent,
            'outputOptions' => $outputOptions
        ]);
    }

    /**
     * 使用预设配置生成海报
     * 
     * @param string $preset 预设名称 (elements, element-only, full, pdf-only)
     * @param string $htmlFile HTML文件路径
     * @param string $outputPrefix 输出文件名前缀
     * @param array $templateData 模板数据 [可选]
     * @param array $options 附加选项 [可选]
     * @return array 生成结果
     */
    public function generatePosterWithPreset($preset, $htmlFile, $outputPrefix = null, array $templateData = null, array $options = [])
    {
        // 合并默认选项
        $options = array_merge([
            'transparent' => true,
            'returnAbsolutePaths' => true,
            'cleanup' => true
        ], $options);

        // 验证HTML文件是否存在
        $htmlPath = $this->resolvePath($htmlFile);
        if (!file_exists($htmlPath)) {
            throw new \Exception("HTML文件不存在: {$htmlPath}");
        }

        // 生成输出文件前缀
        if (!$outputPrefix) {
            $outputPrefix = pathinfo($htmlFile, PATHINFO_FILENAME) . '_' . time();
        }

        // 处理模板数据
        $templateFile = null;
        if ($templateData) {
            $templateFile = $this->createTemplateDataFile($templateData);
        }

        // 构建简化命令
        $command = $this->buildPresetCommand($preset, $htmlFile, $outputPrefix, $templateFile);

        // 执行命令
        $result = $this->executeCommand($command);

        // 根据需要清理临时文件
        if ($options['cleanup']) {
            $this->cleanupTempFiles();
        }

        // 根据预设准备输出文件列表
        $outputFilesResult = $this->prepareOutputFilesForPreset($preset, $outputPrefix);
        $outputFiles = $outputFilesResult['files'];
        $filesExist = $outputFilesResult['exist'];

        // 检查实际图片是否生成
        $haveGeneratedFiles = false;
        foreach ($outputFiles as $file) {
            if ($file && file_exists($file)) {
                $haveGeneratedFiles = true;
                break;
            }
        }

        // 格式化返回结果
        return [
            'success' => $haveGeneratedFiles, // 只要有图片生成就算成功
            'files' => $options['returnAbsolutePaths'] ? $outputFiles : array_map('basename', $outputFiles),
            'output' => $this->debug ? $result['output'] : null,
            'error' => $haveGeneratedFiles ? null : $result['error']
        ];
    }

    /**
     * 直接从HTML字符串生成图片 (适用于动态生成的HTML内容)
     * 
     * @param string $htmlContent HTML字符串内容
     * @param array $templateData 模板数据 [可选]
     * @param array $options 附加选项 [可选]
     * @return array 生成结果
     */
    public function generateFromHtmlContent($htmlContent, array $templateData = null, array $options = [])
    {
        // 合并默认选项
        $options = array_merge([
            'outputPrefix' => 'html_content_' . time(),
            'transparent' => true,
            'preset' => null,
            'outputOptions' => null,
            'returnAbsolutePaths' => true,
            'cleanup' => true
        ], $options);

        // 创建临时HTML文件
        $tempHtmlFile = $this->createTempFile('html', $htmlContent);

        // 生成图片
        $result = $this->generatePoster(
            $tempHtmlFile,
            $templateData,
            $options
        );

        return $result;
    }

    /**
     * 创建临时模板数据文件
     * 
     * @param array $templateData 模板数据
     * @return string 临时文件路径
     */
    private function createTemplateDataFile(array $templateData)
    {
        // 创建一个JSON文件而不是JS文件
        $jsonString = json_encode($templateData, JSON_UNESCAPED_UNICODE);

        if ($this->debug) {
            echo "原始JSON数据: " . $jsonString . "\n";
        }

        // 返回临时JSON文件路径
        return $this->createTempFile('json', $jsonString);
    }

    /**
     * 创建临时文件
     * 
     * @param string $extension 文件扩展名
     * @param string $content 文件内容
     * @return string 临时文件路径
     */
    private function createTempFile($extension, $content)
    {
        // 检测是否为Windows系统
        $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';

        if ($isWindows) {
            // Windows系统下使用当前目录下的temp文件夹
            $tempDir = __DIR__ . DIRECTORY_SEPARATOR . 'temp';
            if (!file_exists($tempDir)) {
                mkdir($tempDir, 0777, true);
            }
            $tempFile = $tempDir . DIRECTORY_SEPARATOR . 'html2img_' . uniqid() . '.' . $extension;
        } else {
            // 非Windows系统使用系统临时目录
            $tempFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'html2img_' . uniqid() . '.' . $extension;
        }

        file_put_contents($tempFile, $content);
        $this->tempFiles[] = $tempFile;

        if ($this->debug) {
            echo "创建临时文件: {$tempFile}\n";
        }

        return $tempFile;
    }

    /**
     * 规范化路径，确保路径在Windows环境下正确处理
     * 
     * @param string $path 原始路径
     * @return string 处理后的路径
     */
    private function normalizePath($path)
    {
        // 检测是否为Windows系统
        $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';

        if ($isWindows) {
            // 将Windows反斜杠替换为正斜杠
            $path = str_replace('\\', '/', $path);

            // 如果是绝对路径，确保驱动器号格式正确
            if (preg_match('/^[a-zA-Z]:\//', $path)) {
                // 已经是正确格式，不需处理
            } else if (preg_match('/^[a-zA-Z]:\\\\/', $path)) {
                // 将C:\ 格式转换为 C:/
                $path = preg_replace('/^([a-zA-Z]):\\\\/', '$1:/', $path);
            }
        }

        return $path;
    }

    /**
     * 构建命令
     * 
     * @param string $htmlFile HTML文件路径
     * @param string $outputPrefix 输出文件名前缀
     * @param bool $transparent 是否使用透明背景
     * @param string $templateFile 模板数据文件路径 [可选]
     * @param array $outputOptions 输出选项 [可选]
     * @return string 完整命令
     */
    private function buildCommand($htmlFile, $outputPrefix, $transparent, $templateFile = null, $outputOptions = null)
    {
        // 检测操作系统类型
        $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';

        // 基础命令
        $command = escapeshellarg($this->nodePath) . ' ' . escapeshellarg($this->scriptPath);

        // 规范化HTML文件路径
        $htmlFile = $this->normalizePath($htmlFile);

        // 添加HTML文件路径
        $command .= ' ' . escapeshellarg($htmlFile);

        // 添加输出前缀
        if ($outputPrefix) {
            $command .= ' --output ' . escapeshellarg($outputPrefix);
        }

        // 添加透明度选项
        $command .= ' --transparent ' . ($transparent ? 'true' : 'false');

        // 添加模板数据参数
        if ($templateFile) {
            // 规范化模板文件路径
            $templateFile = $this->normalizePath($templateFile);

            // 使用dataFile参数直接传递文件路径
            $command .= ' --dataFile ' . escapeshellarg($templateFile);
        }

        // 添加输出选项参数
        if ($outputOptions) {
            // 创建临时文件存储选项
            $optionsFile = $this->createTempFile('json', json_encode($outputOptions, JSON_UNESCAPED_UNICODE));

            // 规范化选项文件路径
            $optionsFile = $this->normalizePath($optionsFile);

            $command .= ' --optionsFile ' . escapeshellarg($optionsFile);
        }

        if ($this->debug) {
            echo "构建的命令: {$command}\n";
        }

        return $command;
    }

    /**
     * 构建预设命令
     * 
     * @param string $preset 预设名称
     * @param string $htmlFile HTML文件路径
     * @param string $outputPrefix 输出文件名前缀
     * @param string $templateFile 模板数据文件路径 [可选]
     * @return string 完整命令
     */
    private function buildPresetCommand($preset, $htmlFile, $outputPrefix, $templateFile = null)
    {
        // 检测操作系统类型
        $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';

        // 基础命令
        $command = escapeshellarg($this->nodePath) . ' ' . escapeshellarg($this->scriptPath);

        // 添加预设命令
        $command .= ' ' . $preset;

        // 规范化HTML文件路径
        $htmlFile = $this->normalizePath($htmlFile);

        // 添加HTML文件路径
        $command .= ' ' . escapeshellarg($htmlFile);

        // 添加输出前缀
        if ($outputPrefix) {
            $command .= ' --output ' . escapeshellarg($outputPrefix);
        }

        // 添加模板数据参数
        if ($templateFile) {
            // 规范化模板文件路径
            $templateFile = $this->normalizePath($templateFile);

            // 使用dataFile参数直接传递文件路径
            $command .= ' --dataFile ' . escapeshellarg($templateFile);
        }

        if ($this->debug) {
            echo "构建的预设命令: {$command}\n";
        }

        return $command;
    }

    /**
     * 执行命令
     * 
     * @param string $command 命令
     * @return array 执行结果
     */
    private function executeCommand($command)
    {
        if ($this->debug) {
            echo "执行命令: {$command}\n";
        }

        // 检测是否为Windows系统
        $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';

        // 设置执行超时时间
        $timeStart = microtime(true);
        $output = [];
        $returnVar = 0;

        // 检查proc_open函数是否可用
        if (!function_exists('proc_open') || in_array('proc_open', explode(',', ini_get('disable_functions')))) {
            // proc_open被禁用，使用替代方案
            if ($this->debug) {
                echo "proc_open函数被禁用，使用替代方案...\n";
            }

            // 创建一个唯一的任务ID
            $taskId = uniqid('task_');

            // 将命令写入临时文件
            $commandFile = $this->createTempFile('cmd', $command);
            $outputFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $taskId . '.output';

            // 使用shell_exec或exec（如果可用）
            if (function_exists('shell_exec') && !in_array('shell_exec', explode(',', ini_get('disable_functions')))) {
                // 重定向输出到文件
                $shellCommand = $command . ' > ' . escapeshellarg($outputFile) . ' 2>&1';
                shell_exec($shellCommand);

                // 读取输出文件
                if (file_exists($outputFile)) {
                    $content = file_get_contents($outputFile);
                    $output = explode("\n", $content);
                    @unlink($outputFile); // 删除输出文件
                } else {
                    $returnVar = 1;
                    $output = ["无法执行命令：输出文件不存在"];
                }
            } elseif (function_exists('exec') && !in_array('exec', explode(',', ini_get('disable_functions')))) {
                // 使用exec作为备选方案
                $shellCommand = $command . ' > ' . escapeshellarg($outputFile) . ' 2>&1';
                exec($shellCommand, $output, $returnVar);

                // 读取可能存在的输出文件
                if (file_exists($outputFile)) {
                    $content = file_get_contents($outputFile);
                    $output = array_merge($output, explode("\n", $content));
                    @unlink($outputFile); // 删除输出文件
                }
            } elseif (function_exists('passthru') && !in_array('passthru', explode(',', ini_get('disable_functions')))) {
                // 使用passthru作为第三选择
                ob_start();
                $shellCommand = $command . ' 2>&1';
                passthru($shellCommand, $returnVar);
                $output = explode("\n", ob_get_clean());
            } elseif (function_exists('system') && !in_array('system', explode(',', ini_get('disable_functions')))) {
                // 使用system作为第四选择
                ob_start();
                $shellCommand = $command . ' 2>&1';
                system($shellCommand, $returnVar);
                $output = explode("\n", ob_get_clean());
            } else {
                $returnVar = 1;
                $output = ["所有命令执行函数均被禁用，无法执行外部命令"];
            }

            // 清理临时文件
            @unlink($commandFile);
        } else if ($isWindows) {
            // Windows系统使用直接执行方式，不使用shell
            // 原有Windows代码保持不变
            $nodePath = trim($this->nodePath, '"\'');
            $scriptPath = $this->normalizePath($this->scriptPath);

            // 解析命令行参数
            preg_match_all('/"([^"]*)"/', $command, $matches);
            $params = [];
            foreach ($matches[1] as $i => $param) {
                if ($i > 1) { // 跳过前两个参数（node路径和脚本路径）
                    $params[] = $param;
                }
            }

            // 构建使用PHP内置函数的命令调用
            $nodeProc = proc_open(
                $nodePath,
                [
                    0 => ["pipe", "r"],  // stdin
                    1 => ["pipe", "w"],  // stdout
                    2 => ["pipe", "w"]   // stderr
                ],
                $pipes,
                null,
                [
                    'NODE_PATH' => dirname($scriptPath) . DIRECTORY_SEPARATOR . 'node_modules'
                ]
            );

            if (is_resource($nodeProc)) {
                // 关闭管道
                fclose($pipes[0]);
                fclose($pipes[1]);
                fclose($pipes[2]);

                // 获取退出码
                $returnVar = proc_close($nodeProc);

                // 直接执行脚本
                $phpOutput = shell_exec('node "' . $scriptPath . '" "' . implode('" "', $params) . '"');
                $output = $phpOutput ? explode("\n", $phpOutput) : [];
            } else {
                $returnVar = 1;
                $output = ["无法启动Node.js进程"];
            }
        } else {
            // Linux系统使用进程控制
            // 原有Linux代码保持不变
            // 使用proc_open替代exec以获得更好的进程控制
            $descriptorspec = [
                0 => ["pipe", "r"],  // stdin
                1 => ["pipe", "w"],  // stdout
                2 => ["pipe", "w"]   // stderr
            ];

            // 设置进程执行环境变量
            $env = [
                'NODE_OPTIONS' => '--max-old-space-size=512', // 限制Node.js内存使用
                'PATH' => getenv('PATH')
            ];

            $process = proc_open($command, $descriptorspec, $pipes, null, $env);

            if (is_resource($process)) {
                // 设置非阻塞模式
                stream_set_blocking($pipes[1], 0);
                stream_set_blocking($pipes[2], 0);

                // 设置超时(10秒)
                $timeout = 10;
                $startTime = time();

                $stdout = '';
                $stderr = '';

                // 读取输出直到进程结束或超时
                do {
                    $status = proc_get_status($process);

                    // 读取标准输出
                    $stdout .= stream_get_contents($pipes[1]);
                    $stderr .= stream_get_contents($pipes[2]);

                    // 检查是否超时
                    if (time() - $startTime > $timeout) {
                        // 超时，杀死进程
                        proc_terminate($process, 9);
                        if ($this->debug) {
                            echo "命令执行超时，已终止\n";
                        }
                        $returnVar = 1;
                        $output = ["执行超时，已强制终止"];
                        break;
                    }

                    // 短暂休眠以减少CPU占用
                    usleep(100000); // 100ms

                } while ($status['running']);

                // 关闭管道
                fclose($pipes[0]);
                fclose($pipes[1]);
                fclose($pipes[2]);

                // 获取最终退出码
                if (!isset($returnVar) || $returnVar === 0) {
                    $returnVar = proc_close($process);
                } else {
                    proc_close($process);
                }

                // 处理输出
                if (!empty($stdout)) {
                    $output = explode("\n", $stdout);
                }

                // 处理错误
                if (!empty($stderr) && $returnVar !== 0) {
                    $output = array_merge($output, explode("\n", $stderr));
                }
            } else {
                $returnVar = 1;
                $output = ["无法启动进程"];
            }
        }

        $timeEnd = microtime(true);
        $executionTime = round($timeEnd - $timeStart, 2);

        if ($this->debug) {
            echo "命令执行时间: {$executionTime}秒\n";
            echo "命令返回代码: {$returnVar}\n";
            echo "命令输出:\n";
            echo implode("\n", $output) . "\n";
        }

        // 即使不在调试模式下，也输出错误信息
        if ($returnVar !== 0) {
            echo "命令执行失败，返回代码: {$returnVar}，执行时间: {$executionTime}秒\n";
            echo "错误信息: " . implode("\n", $output) . "\n";
        }

        return [
            'success' => ($returnVar === 0),
            'output' => $output,
            'error' => ($returnVar !== 0) ? implode("\n", $output) : null,
            'execution_time' => $executionTime
        ];
    }

    /**
     * 准备输出文件列表并验证文件是否存在
     * 
     * @param string $outputPrefix 输出文件名前缀
     * @param bool $transparent 是否使用透明背景
     * @param array $outputOptions 输出选项 [可选]
     * @return array 输出文件路径和存在状态
     */
    private function prepareOutputFiles($outputPrefix, $transparent, $outputOptions = null)
    {
        // 合并默认输出选项
        $options = array_merge($this->defaultOutputOptions, (array) $outputOptions);

        // 构建输出文件路径
        $files = [
            'fullPage' => $options['fullPage'] ? $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_full.png' : null,
            'element' => $options['element'] ? $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_element.png' : null,
            'elementWithBg' => ($options['elementWithBg'] && $transparent) ? $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_element_with_bg.png' : null,
            'pdf' => $options['pdf'] ? $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '.pdf' : null
        ];

        // 检查实际文件是否存在，可能命令执行返回错误但文件已生成
        $filesExist = false;
        foreach ($files as $type => $path) {
            if ($path && file_exists($path)) {
                $filesExist = true;
                break;
            }
        }

        if ($this->debug && $filesExist) {
            echo "检测到至少一个输出文件存在\n";
        }

        return [
            'files' => $files,
            'exist' => $filesExist
        ];
    }

    /**
     * 根据预设准备输出文件列表
     * 
     * @param string $preset 预设名称
     * @param string $outputPrefix 输出文件名前缀
     * @return array 输出文件路径
     */
    private function prepareOutputFilesForPreset($preset, $outputPrefix)
    {
        $files = [];

        switch ($preset) {
            case 'elements':
                $files = [
                    'element' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_element.png',
                    'elementWithBg' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_element_with_bg.png'
                ];
                break;
            case 'element-only':
                $files = [
                    'element' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_element.png'
                ];
                break;
            case 'full':
                $files = [
                    'fullPage' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_full.png',
                    'element' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_element.png',
                    'elementWithBg' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '_element_with_bg.png',
                    'pdf' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '.pdf'
                ];
                break;
            case 'pdf-only':
                $files = [
                    'pdf' => $this->outputDir . DIRECTORY_SEPARATOR . $outputPrefix . '.pdf'
                ];
                break;
            default:
                $files = [];
        }

        // 检查实际文件是否存在，可能命令执行返回错误但文件已生成
        $filesExist = false;
        foreach ($files as $type => $path) {
            if ($path && file_exists($path)) {
                $filesExist = true;
                break;
            }
        }

        if ($this->debug && $filesExist) {
            echo "检测到至少一个预设输出文件存在\n";
        }

        return [
            'files' => $files,
            'exist' => $filesExist
        ];
    }

    /**
     * 清理临时文件
     */
    private function cleanupTempFiles()
    {
        foreach ($this->tempFiles as $file) {
            if (file_exists($file)) {
                unlink($file);
                if ($this->debug) {
                    echo "已删除临时文件: {$file}\n";
                }
            }
        }

        $this->tempFiles = [];
    }

    /**
     * 解析文件路径
     * 
     * @param string $path 文件路径
     * @return string 完整路径
     */
    private function resolvePath($path)
    {
        // 如果是绝对路径，直接返回
        if (file_exists($path)) {
            return $path;
        }

        // 否则尝试相对于当前目录解析
        $absolutePath = __DIR__ . DIRECTORY_SEPARATOR . $path;
        if (file_exists($absolutePath)) {
            return $absolutePath;
        }

        // 返回原始路径，由调用方法处理路径不存在的情况
        return $path;
    }

    /**
     * 设置调试模式
     * 
     * @param bool $debug 是否启用调试模式
     * @return $this
     */
    public function setDebug($debug)
    {
        $this->debug = (bool) $debug;
        return $this;
    }

    /**
     * 设置输出目录
     * 
     * @param string $outputDir 输出目录路径
     * @return $this
     */
    public function setOutputDir($outputDir)
    {
        $this->outputDir = $outputDir;

        // 确保输出目录存在
        if (!file_exists($this->outputDir)) {
            mkdir($this->outputDir, 0777, true);
        }

        return $this;
    }

    /**
     * 设置默认输出选项
     * 
     * @param array $options 输出选项
     * @return $this
     */
    public function setDefaultOutputOptions(array $options)
    {
        $this->defaultOutputOptions = array_merge($this->defaultOutputOptions, $options);
        return $this;
    }

    /**
     * 获取输出目录路径
     * 
     * @return string
     */
    public function getOutputDir()
    {
        return $this->outputDir;
    }

    /**
     * 获取默认输出选项
     * 
     * @return array
     */
    public function getDefaultOutputOptions()
    {
        return $this->defaultOutputOptions;
    }

    /**
     * 设置默认域名
     * 
     * @param string $domain 默认域名
     * @return $this
     */
    public function setDefaultDomain($domain)
    {
        $this->defaultDomain = $domain;
        return $this;
    }

    /**
     * 获取默认域名
     * 
     * @return string
     */
    public function getDefaultDomain()
    {
        return $this->defaultDomain;
    }

    /**
     * 从HTML模板文件生成图片
     * 
     * 使用指定的HTML模板文件和模板数据生成图片
     * 
     * @param string $templateFile HTML模板文件路径
     * @param string $outputPrefix 输出文件名前缀
     * @param array $templateData 模板数据
     * @param bool $transparent 是否使用透明背景
     * @param array $outputOptions 输出选项
     * @return array 生成结果
     */
    public function generateFromTemplate($templateFile, $outputPrefix, array $templateData, $transparent = true, $outputOptions = null)
    {
        return $this->generatePoster($templateFile, $templateData, [
            'outputPrefix' => $outputPrefix,
            'transparent' => $transparent,
            'outputOptions' => $outputOptions
        ]);
    }

    /**
     * 从HTML字符串生成图片
     * 
     * 直接从HTML字符串内容生成图片，无需创建模板文件
     * 
     * @param string $htmlString HTML字符串内容
     * @param string $outputPrefix 输出文件名前缀
     * @param bool $transparent 是否使用透明背景
     * @param array $outputOptions 输出选项
     * @return array 生成结果
     */
    public function generateFromHtmlString($htmlString, $outputPrefix, $transparent = true, $outputOptions = null)
    {
        return $this->generateFromHtmlContent($htmlString, null, [
            'outputPrefix' => $outputPrefix,
            'transparent' => $transparent,
            'outputOptions' => $outputOptions
        ]);
    }
}